﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using DME.Json.Utilities;

namespace DME.Json.Bson
{
    internal class BsonBinaryWriter
    {
        private static readonly Encoding Encoding = Encoding.UTF8;

        private readonly BinaryWriter _writer;

        private byte[] _largeByteBuffer;
        private int _maxChars;

        public DateTimeKind DateTimeKindHandling { get; set; }

        public BsonBinaryWriter(Stream stream)
        {
            DateTimeKindHandling = DateTimeKind.Utc;
            _writer = new BinaryWriter(stream);
        }

        public void Flush()
        {
            _writer.Flush();
        }

        public void WriteToken(BsonToken t)
        {
            CalculateSize(t);
            WriteTokenInternal(t);
        }

        private void WriteTokenInternal(BsonToken t)
        {
            switch (t.Type)
            {
                case BsonType.Object:
                    {
                        BsonObject value = (BsonObject)t;
                        _writer.Write(value.CalculatedSize);
                        foreach (BsonProperty property in value)
                        {
                            _writer.Write((sbyte)property.Value.Type);
                            WriteString((string)property.Name.Value, property.Name.ByteCount, null);
                            WriteTokenInternal(property.Value);
                        }
                        _writer.Write((byte)0);
                    }
                    break;
                case BsonType.Array:
                    {
                        BsonArray value = (BsonArray)t;
                        _writer.Write(value.CalculatedSize);
                        int index = 0;
                        foreach (BsonToken c in value)
                        {
                            _writer.Write((sbyte)c.Type);
                            WriteString(index.ToString(CultureInfo.InvariantCulture), MathUtils.IntLength(index), null);
                            WriteTokenInternal(c);
                            index++;
                        }
                        _writer.Write((byte)0);
                    }
                    break;
                case BsonType.Integer:
                    {
                        BsonValue value = (BsonValue)t;
                        _writer.Write(Convert.ToInt32(value.Value, CultureInfo.InvariantCulture));
                    }
                    break;
                case BsonType.Long:
                    {
                        BsonValue value = (BsonValue)t;
                        _writer.Write(Convert.ToInt64(value.Value, CultureInfo.InvariantCulture));
                    }
                    break;
                case BsonType.Number:
                    {
                        BsonValue value = (BsonValue)t;
                        _writer.Write(Convert.ToDouble(value.Value, CultureInfo.InvariantCulture));
                    }
                    break;
                case BsonType.String:
                    {
                        BsonString value = (BsonString)t;
                        WriteString((string)value.Value, value.ByteCount, value.CalculatedSize - 4);
                    }
                    break;
                case BsonType.Boolean:
                    {
                        BsonValue value = (BsonValue)t;
                        _writer.Write((bool)value.Value);
                    }
                    break;
                case BsonType.Null:
                case BsonType.Undefined:
                    break;
                case BsonType.Date:
                    {
                        BsonValue value = (BsonValue)t;

                        long ticks = 0;

                        if (value.Value is DateTime)
                        {
                            DateTime dateTime = (DateTime)value.Value;
                            if (DateTimeKindHandling == DateTimeKind.Utc)
                                dateTime = dateTime.ToUniversalTime();
                            else if (DateTimeKindHandling == DateTimeKind.Local)
                                dateTime = dateTime.ToLocalTime();

                            ticks = JsonConvert.ConvertDateTimeToJavaScriptTicks(dateTime, false);
                        }
#if !PocketPC && !NET20
                        else
                        {
                            DateTimeOffset dateTimeOffset = (DateTimeOffset)value.Value;
                            ticks = JsonConvert.ConvertDateTimeToJavaScriptTicks(dateTimeOffset.UtcDateTime, dateTimeOffset.Offset);
                        }
#endif

                        _writer.Write(ticks);
                    }
                    break;
                case BsonType.Binary:
                    {
                        BsonValue value = (BsonValue)t;

                        byte[] data = (byte[])value.Value;
                        _writer.Write(data.Length);
                        _writer.Write((byte)BsonBinaryType.Data);
                        _writer.Write(data);
                    }
                    break;
                case BsonType.Oid:
                    {
                        BsonValue value = (BsonValue)t;

                        byte[] data = (byte[])value.Value;
                        _writer.Write(data);
                    }
                    break;
                case BsonType.Regex:
                    {
                        BsonRegex value = (BsonRegex)t;

                        WriteString((string)value.Pattern.Value, value.Pattern.ByteCount, null);
                        WriteString((string)value.Options.Value, value.Options.ByteCount, null);
                    }
                    break;
                default:
                    throw new ArgumentOutOfRangeException("t", "Unexpected token when writing BSON: {0}".FormatWith(CultureInfo.InvariantCulture, t.Type));
            }
        }

        private void WriteString(string s, int byteCount, int? calculatedlengthPrefix)
        {
            if (calculatedlengthPrefix != null)
                _writer.Write(calculatedlengthPrefix.Value);

            if (s != null)
            {
                if (_largeByteBuffer == null)
                {
                    _largeByteBuffer = new byte[256];
                    _maxChars = 256 / Encoding.GetMaxByteCount(1);
                }
                if (byteCount <= 256)
                {
                    Encoding.GetBytes(s, 0, s.Length, _largeByteBuffer, 0);
                    _writer.Write(_largeByteBuffer, 0, byteCount);
                }
                else
                {
                    int charCount;
                    int totalCharsWritten = 0;
                    for (int i = s.Length; i > 0; i -= charCount)
                    {
                        charCount = (i > _maxChars) ? _maxChars : i;
                        int count = Encoding.GetBytes(s, totalCharsWritten, charCount, _largeByteBuffer, 0);
                        _writer.Write(_largeByteBuffer, 0, count);
                        totalCharsWritten += charCount;
                    }
                }
            }

            _writer.Write((byte)0);
        }

        private int CalculateSize(int stringByteCount)
        {
            return stringByteCount + 1;
        }

        private int CalculateSizeWithLength(int stringByteCount, bool includeSize)
        {
            int baseSize = (includeSize)
              ? 5 // size bytes + terminator
              : 1; // terminator

            return baseSize + stringByteCount;
        }

        private int CalculateSize(BsonToken t)
        {
            switch (t.Type)
            {
                case BsonType.Object:
                    {
                        BsonObject value = (BsonObject)t;

                        int bases = 4;
                        foreach (BsonProperty p in value)
                        {
                            int size = 1;
                            size += CalculateSize(p.Name);
                            size += CalculateSize(p.Value);

                            bases += size;
                        }
                        bases += 1;
                        value.CalculatedSize = bases;
                        return bases;
                    }
                case BsonType.Array:
                    {
                        BsonArray value = (BsonArray)t;

                        int size = 4;
                        int index = 0;
                        foreach (BsonToken c in value)
                        {
                            size += 1;
                            size += CalculateSize(MathUtils.IntLength(index));
                            size += CalculateSize(c);
                            index++;
                        }
                        size += 1;
                        value.CalculatedSize = size;

                        return value.CalculatedSize;
                    }
                case BsonType.Integer:
                    return 4;
                case BsonType.Long:
                    return 8;
                case BsonType.Number:
                    return 8;
                case BsonType.String:
                    {
                        BsonString value = (BsonString)t;
                        string s = (string)value.Value;
                        value.ByteCount = (s != null) ? Encoding.GetByteCount(s) : 0;
                        value.CalculatedSize = CalculateSizeWithLength(value.ByteCount, value.IncludeLength);

                        return value.CalculatedSize;
                    }
                case BsonType.Boolean:
                    return 1;
                case BsonType.Null:
                case BsonType.Undefined:
                    return 0;
                case BsonType.Date:
                    return 8;
                case BsonType.Binary:
                    {
                        BsonValue value = (BsonValue)t;

                        byte[] data = (byte[])value.Value;
                        value.CalculatedSize = 4 + 1 + data.Length;

                        return value.CalculatedSize;
                    }
                case BsonType.Oid:
                    return 12;
                case BsonType.Regex:
                    {
                        BsonRegex value = (BsonRegex)t;
                        int size = 0;
                        size += CalculateSize(value.Pattern);
                        size += CalculateSize(value.Options);
                        value.CalculatedSize = size;

                        return value.CalculatedSize;
                    }
                default:
                    throw new ArgumentOutOfRangeException("t", "Unexpected token when writing BSON: {0}".FormatWith(CultureInfo.InvariantCulture, t.Type));
            }
        }
    }
}